﻿/// -----------------------------------------------------------------------
/// (c) 2009 Microsoft Corporation -- All rights reserved
/// This code is licensed under the MS-PL
/// http://www.opensource.org/licenses/ms-pl.html
/// Courtesy of the Open Source Techology Center: http://port25.technet.com
/// -----------------------------------------------------------------------


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Text;
using Microsoft.Win32.SafeHandles;
using gsToolkit;

namespace SynthFastCGI {
    class Server {
        public int Iterations { get; set; }
        public string CgiScript { get; set; }
        public string OutputFilename{ get; set; }
        public string OutputHeadersFilename{ get; set; }
        public string CgiExe { get; set; }
        public string Before { get; set; }
        public string After { get; set; }

        public string PipeName = "\\\\.\\pipe\\FastCGI\\FakeFastCGI";
        public Dictionary<string, string> CgiParameters = new Dictionary<string, string>();
        private TextWriter responseDocument;
        private TextWriter responseHeaders;

        public List<string> Extensions = new List<string>();
        public List<string> Requests = new List<string>();
        public List<Cookie> cookies = new List<Cookie>();

        public string CookieFile;
        public bool SaveCookies;
        public string DocumentRoot;

        private STARTUPINFO StartInfo;
        private PROCESS_INFORMATION ProcessInfo;
        private SECURITY_ATTRIBUTES SecurityAttributes;
        private SafeFileHandle ServerPipe;
        private SafeFileHandle ClientPipe;
        private FileStream clientPipeStream;

        private Message stdIn;
        private Message beginRequest;

        private byte[] responseHeader;
        private byte[] responseBuffer;
        private StringBuilder response;

        public string CookieData {
            get {
                return CookieExtensions.GetEncodedCookies(cookies);
            }
        }

        public Server() {
            responseDocument = null;
            responseHeaders = null;
            Iterations = 1;
            CookieFile = Path.Combine(System.Environment.CurrentDirectory, "cookies.txt");

            CgiParameters["HTTP_ACCEPT"] = "*/*";
            CgiParameters["HTTP_HOST"] = "localhost";
            CgiParameters["REQUEST_METHOD"] = "GET";
            CgiParameters["CONTENT_LENGTH"] = "0";
            CgiParameters["HTTPS"] = "off";
            CgiParameters["INSTANCE_ID"] = "1";
            CgiParameters["INSTANCE_META_PATH"] = "/LM/W3SVC/1";
            CgiParameters["REMOTE_ADDR"] = "127.0.0.1";
            CgiParameters["REMOTE_HOST"] = "127.0.0.1";
            CgiParameters["SERVER_NAME"] = "localhost";
            CgiParameters["SERVER_PORT"] = "80";
            CgiParameters["SERVER_PORT_SECURE"] = "0";
            CgiParameters["SERVER_PROTOCOL"] = "HTTP/1.1";
            CgiParameters["SERVER_SOFTWARE"] = "Microsoft-IIS/6.0";

            stdIn = new Message(FastCGI.FCGI_STDIN, 0xaa, 0xaa);

            beginRequest = new Message(FastCGI.FCGI_BEGIN_REQUEST, 0xaa, 0xaa);
            beginRequest.Append(0);
            beginRequest.Append(FastCGI.FCGI_RESPONDER);
            beginRequest.Append(FastCGI.FCGI_KEEP_CONN);
            beginRequest.PadTo(8);

            responseHeader = new byte[8];
            responseBuffer = new byte[65536];
        }


        public int Run() {
            if(File.Exists(CookieFile)) {
                CookieExtensions.LoadCookiesFromNSFile(cookies, CookieFile);
            }

            int result = 0;
            StartInfo = new STARTUPINFO();
            ProcessInfo = new PROCESS_INFORMATION();
            SecurityAttributes = new SECURITY_ATTRIBUTES();
            SecurityAttributes.length = System.Runtime.InteropServices.Marshal.SizeOf(SecurityAttributes);
            SecurityAttributes.bInheritHandle = true;

            ServerPipe = new SafeFileHandle(IntPtr.Zero, true);
            ClientPipe = new SafeFileHandle(IntPtr.Zero, true);
            clientPipeStream = null;

            try {
                this.ServerPipe = Win32.CreateNamedPipe(PipeName, Win32.PIPE_ACCESS_DUPLEX, Win32.PIPE_TYPE_BYTE | Win32.PIPE_WAIT | Win32.PIPE_READMODE_BYTE, Win32.PIPE_UNLIMITED_INSTANCES, 4096, 4096, 0, ref SecurityAttributes);

                if(!this.ServerPipe.IsInvalid) {
                    this.ClientPipe = Win32.CreateFile(PipeName, Win32.GENERIC_READ | Win32.GENERIC_WRITE, 0, IntPtr.Zero, Win32.OPEN_EXISTING, 0, IntPtr.Zero);
                    if (!this.ClientPipe.IsInvalid) {
                        this.clientPipeStream = new FileStream(this.ClientPipe, FileAccess.ReadWrite, 4096, false);

                        StartInfo.cb = (uint) System.Runtime.InteropServices.Marshal.SizeOf(StartInfo);
                        StartInfo.dwFlags = 0x100; //STARTF_USESTDHANDLES
                        StartInfo.hStdInput = this.ServerPipe;
                        StartInfo.hStdError = -1;
                        StartInfo.hStdOutput = -1;

                        if (!Win32.CreateProcess(null, CgiExe, IntPtr.Zero, IntPtr.Zero, true, 0, IntPtr.Zero, null, ref StartInfo, out ProcessInfo)) {
                            Console.WriteLine("CreateProc Failed.");
                            return -1;
                        }

                        if(!string.IsNullOrEmpty(Before)) {
                            ProcessStartInfo psi = new ProcessStartInfo() {
                                FileName = Before
                            };
                            Process.Start(psi).WaitForExit();
                        }


                        ServerPipe.Close();
                        ServerPipe = new SafeFileHandle(IntPtr.Zero, true);

                        if (string.IsNullOrEmpty(CgiScript))
                            result = RunRequestMode();
                        else
                            result = RunScriptMode();
                    }
                }
            }

            finally
            {
                if(!string.IsNullOrEmpty(After)) {
                    // exec.
                    ProcessStartInfo psi = new ProcessStartInfo() {
                        FileName = After
                    };
                    Process.Start(psi).WaitForExit();
                }

                try {
                    Process.GetProcessById(ProcessInfo.dwProcessId).Kill();

                    if( this.clientPipeStream != null )
                        this.clientPipeStream.Close();

                    if(!this.ClientPipe.IsInvalid)
                        this.ClientPipe.Close();

                    if(!this.ServerPipe.IsInvalid)
                        this.ServerPipe.Close();
                }
                catch {
                }
            }

            return result;
        }

        private int RunScriptMode() {
            var requestParams = new Message(FastCGI.FCGI_PARAMS, 0xaa, 0xaa);
            foreach(string key in CgiParameters.Keys) {
                requestParams.Append(key, CgiParameters[key]);
            }

            for(int i = 0;i < Iterations;i++) {
                OpenOutputStreams(CgiScript);

                clientPipeStream.Write(beginRequest);
                clientPipeStream.Write(requestParams);
                clientPipeStream.Write(stdIn);

                HandleResponse();
            }
            return 0;
        }

        private int RunRequestMode() {
            if(string.IsNullOrEmpty(DocumentRoot))
                return Program.Fail("Missing parameter: --document-root or --script must be specified.");

            CgiParameters["HTTP_COOKIE"] = CookieData;

            foreach(string req in Requests) {
                bool ok = false;

                Uri uri = WebExtensions.CanonicalizeUri(req);
                string scriptPath = Path.Combine( DocumentRoot,  uri.LocalPath.Replace("/", "\\").Substring(1) );


                string ext = Path.GetExtension(scriptPath).ToLower();
                if( Extensions.Count == 0 ) {
                    if( !ext.Equals("php") )
                        ok = true;
                }
                else {
                    foreach (var s in Extensions) {
                        if( s.ToLower().Equals(ext) ||  s.ToLower().Equals("."+ext) ) {
                            ok = true;
                            break;
                        }
                    }
                }

                if( !ok )
                    continue;

                if(!File.Exists(scriptPath)) {
                    Console.WriteLine("404: cannot find [{0}] for [{1}]", scriptPath, uri.AbsoluteUri);
                }

                CgiParameters["REQUEST_URI"] = uri.PathAndQuery;
                CgiParameters["QUERY_STRING"] = uri.Query.TrimStart('?');
                CgiParameters["URL"] = uri.LocalPath;
                CgiParameters["SCRIPT_NAME"] = uri.LocalPath;
                CgiParameters["PATH_TRANSLATED"] = scriptPath;
                CgiParameters["DOCUMENT_ROOT"] = DocumentRoot;

                var requestParams = new Message(FastCGI.FCGI_PARAMS, 0xaa, 0xaa);
                foreach(string key in CgiParameters.Keys) {
                    requestParams.Append(key, CgiParameters[key]);
                }

                OpenOutputStreams(req);

                clientPipeStream.Write(beginRequest);
                clientPipeStream.Write(requestParams);
                clientPipeStream.Write(stdIn);

                HandleResponse();
            }
            return 0;
        }

        private void HandleResponse() {
            response = new StringBuilder();

            do {

                int bytesRead = this.clientPipeStream.Read(this.responseHeader, 0, 8);
                if(bytesRead != 8)
                    throw new Exception("Invalid Response Header");

                int responseSize = this.responseHeader[5] + (this.responseHeader[4] << 8) + this.responseHeader[6];

                if(responseSize >= this.responseBuffer.Length)
                    this.responseBuffer = new byte[responseSize + 1];

                clientPipeStream.Read(this.responseBuffer, 0, responseSize);

                switch(this.responseHeader[1]) {
                    case FastCGI.FCGI_STDOUT:
                        response.Append(Encoding.ASCII.GetString(this.responseBuffer, 0, this.responseHeader[5] + (this.responseHeader[4] << 8)));
                        break;
                }

            } while(this.responseHeader[1] != FastCGI.FCGI_END_REQUEST);
            response.Replace("\r\n", "\n");
            response.Replace("\r", "\n");

            int pos = 0;
            while(!(response[pos] == '\n' && response[pos+1] == '\n'))
                pos++;
            string headers = response.ToString(0, pos);
            pos += 2;
            string body = response.ToString(pos, response.Length -pos);
            if( responseDocument != null )
                responseDocument.Write( body );
            if( responseHeaders != null )
                responseHeaders.WriteLine(headers);

            CloseOutputStreams();

            List<string> headerList = CollectionExtensions.SplitToList(headers, '\n');
            foreach( string s in headerList ) {
                pos = s.IndexOf(':');
                string key = s.Substring(0, pos);
                string value = s.Substring(pos + 1);
                switch(key.ToLower()) {
                    case "set-cookie":
                        CookieExtensions.AddCookie(cookies, value.Trim());
                        break;
                    case "status":
                        value = value.Trim();
                        pos = 0;
                        while(value[pos] >= '0' && value[pos] <= '9')
                            pos++;
                        int status = value.Substring(0, pos).ToInt32();
                        // could code to follow 302's etc...
                        break;
                }
            }
            if(SaveCookies)
                CookieExtensions.SaveCookiesToNSFile(cookies, CookieFile);
        }

        private void CloseOutputStreams() {
            if(responseDocument != null) {
                responseDocument.Flush();
                if(!(Console.Out == responseDocument))
                    responseDocument.Close();
            }
            if(responseHeaders != null) {
                responseHeaders.Flush();
                if (!(Console.Out == responseHeaders))
                    responseHeaders.Close();
            }
        }

        private void OpenOutputStreams(string filenameHint) {
            if( OutputFilename != "null")
                responseDocument = string.IsNullOrEmpty(OutputFilename) ? Console.Out : new StreamWriter(OutputFilename.CanonicalizePath(filenameHint), false);
            if(OutputHeadersFilename != "null")
                responseHeaders = string.IsNullOrEmpty(OutputHeadersFilename) ? Console.Out : new StreamWriter(OutputHeadersFilename.CanonicalizePath(filenameHint), false);
        }
    }
}
